﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

using King.Extensions;

namespace King.Reflection {

    [
        ComImport,
        Guid("B81FF171-20F3-11d2-8DCC-00A0C9B00525")
    ]
    internal sealed class TypeNameFactory { }

    [
        Guid("B81FF171-20F3-11d2-8DCC-00A0C9B00521"),
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
    ]
    internal interface ITypeNameFactory {
        void ParseTypeName(string name, out int error, out ITypeName typeName);
        ITypeNameBuilder GetTypeNameBuilder();
    }

    [
        Guid("B81FF171-20F3-11d2-8DCC-00A0C9B00522"),
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
    ]
    internal unsafe interface ITypeName {

        int GetNameCount();
        void GetNames(int count, char** names, out int nameCount);
        int GetTypeArgumentCount();
        void GetTypeArguments(int count, IntPtr* ppTypeNames, out int argumentCount);
        int GetModifierLength();
        void GetModifiers(int count, Modifier* modifiers, out int modifiersCount);
        string GetAssemblyName();
    }

    [
        Guid("B81FF171-20F3-11d2-8DCC-00A0C9B00523"),
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
    ]
    internal interface ITypeNameBuilder {
        void OpenGenericArguments();
        void CloseGenericArguments();
        void OpenGenericArgument();
        void CloseGenericArgument();
        void AddName(string name);
        void AddPointer();
        void AddByRef();
        void AddSzArray();
        void AddArray(int rank);
        void AddAssemblySpec(string assemblySpec);
        string ToString();
        void Clear();
    }

    internal enum Modifier {
        Pointer = 0xf,
        Array = 0x14,
        SzArray = 0x1d,
        ByRef = 0x10,
    }

    internal static class TypeNameExtensions {

        internal static void TryParseTypeName(this TypeNameFactory typeNameFactory, string name, out ITypeName typeName, out int error) {
            var factory = (ITypeNameFactory)(object)typeNameFactory;
            factory.ParseTypeName(name, out error, out typeName);
        }
        internal static unsafe string[] Names(this ITypeName typeName) {
            var count = typeName.GetNameCount();
            char** ppNames = stackalloc char*[count];
            typeName.GetNames(count, ppNames, out count);

            var names = new string[count];
            for (int i = 0; i < count; i++) {
                char* pBstr = ppNames[i];
                names[i] = new string(ppNames[i]);
                Marshal.FreeBSTR((IntPtr)pBstr);
            }

            return names;
        }
        internal static unsafe Modifier[] Modifiers(this ITypeName typeName) {
            var count = typeName.GetModifierLength();
            if (count == 0)
                return new Modifier[0];

            Modifier* pModifiers = stackalloc Modifier[count];
            typeName.GetModifiers(count, pModifiers, out count);

            var modifiers = new Modifier[count];
            for (int i = 0; i < count; i++)
                modifiers[i] = pModifiers[i];

            return modifiers;
        }
        internal static unsafe ITypeName[] GenericArguments(this ITypeName typeName) {
            var count = typeName.GetTypeArgumentCount();
            if (count == 0)
                return new ITypeName[0];

            IntPtr* ppTypeNames = stackalloc IntPtr[count];
            typeName.GetTypeArguments(count, ppTypeNames, out count);

            var typeNames = new ITypeName[count];
            for (int i = 0; i < count; i++)
                typeNames[i] = (ITypeName)Marshal.GetObjectForIUnknown(ppTypeNames[i]);

            return typeNames;
        }

        internal static ITypeNameBuilder CreateTypeNameBuilder(this TypeNameFactory typeNameFactory) {
            var factory = (ITypeNameFactory)(object)typeNameFactory;
            return factory.GetTypeNameBuilder();
        }
    }

    public sealed class TypeName {

        public static TypeName TryParse(string name, out int errorCharacter) {
            errorCharacter = 0;

            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException();

            var typeNameFactory = new TypeNameFactory();

            ITypeName iTypeName = null;
            int error;
            typeNameFactory.TryParseTypeName(name, out iTypeName, out error);
            if (error >= 0) {
                errorCharacter = error;
                return null;
            }

            return Create(iTypeName);
        }
        public static TypeName TryParse(string name) {
            int error;
            return TryParse(name, out error);
        }
        public static TypeName Parse(string name) {
            TypeName typeName;
            int error;

            if ((typeName = TryParse(name, out error)) == null)
                    throw new ArgumentException(string.Format(
                        "Enountered an error at character {0} while parsing the type name \"{1}\".",
                        error + 1, name));

            return typeName;
        }

        public static TypeName Create(string name,
            string @namespace = null,
            string assemblyFullName = null,
            IEnumerable<TypeName> genericArguments = null) {

            return new TypeName(assemblyFullName, @namespace, name, genericArguments);
        }

        private static TypeName Create(ITypeName typeName, bool isGenericArgument = false) {
            var names = typeName.Names();
            var assemblySpec = typeName.GetAssemblyName();
            if (assemblySpec == string.Empty)
                assemblySpec = null;

            // every name is a type except for the first one which may contain a namespace
            var ns = default(string);
            var match = Regex.Match(names[0], "^(?<ns>.*)[.](?<name>[^.]*)$");
            if (match.Success) {
                ns = match.Get("ns");
                names[0] = match.Get("name");
            }

            // create all types
            TypeName declaringType = null;
            for (int i = 0; i < names.Length; i++) {
                var type = names[i];
                var isLastName = i == names.Length - 1;
                var genericArguments = isLastName ? typeName.GenericArguments().Select(o => Create(o, true)).ToArray() : null;
                
                if (declaringType == null)
                    declaringType = TypeName.Create(type, ns, assemblySpec, genericArguments);
                else
                    declaringType = declaringType.MakeNestedType(type, genericArguments);
            }

            // add modifiers
            TypeName elementType = declaringType;
            if (!isGenericArgument) {
                var modifiers = typeName.Modifiers();
                for (int i = 0; i < modifiers.Length; i++) {
                    var modifier = modifiers[i];
                    int rank = modifier == Modifier.Array ? (int)modifiers[++i] : 0;
                    elementType = new TypeName(elementType, modifier, rank);
                }
            }

            return elementType;
        }

        private string m_assemblyFullName;
        private string m_namespace;
        private TypeName m_declaringType;
        private string m_name;
        private IEnumerable<TypeName> m_genericArguments;
        private TypeName m_elementType;
        private Modifier m_modifier;
        private int m_rank;
        private string m_fullName;
        private string m_builtName;

        private TypeName(TypeName declaringType, string name, IEnumerable<TypeName> genericArguments) {
            Initialize(declaringType.AssemblyFullName, declaringType.Namespace, declaringType, name, genericArguments);
        }
        private TypeName(string assemblySpec, string ns, string name, IEnumerable<TypeName> genericArguments) {
            Initialize(assemblySpec, ns, null, name, genericArguments);
        }
        private void Initialize(string assemblySpec, string ns, TypeName declaringType, string name, IEnumerable<TypeName> genericArguments) {
            m_assemblyFullName = assemblySpec;
            m_namespace = ns;
            m_declaringType = declaringType;
            m_name = name;
            m_genericArguments = genericArguments;

            m_builtName = BuildTypeName();
        }
        private TypeName(TypeName elementType, Modifier modifier, int rank = 0) {
            var rootElementType = elementType.RootElementType;

            m_assemblyFullName = rootElementType.m_assemblyFullName;
            m_namespace = rootElementType.m_namespace;
            m_name = rootElementType.m_name;
            m_declaringType = rootElementType.m_declaringType;
            m_genericArguments = elementType.m_genericArguments;

            m_elementType = elementType;
            m_modifier = modifier;
            m_rank = rank;

            if (m_modifier == Modifier.SzArray)
                m_rank = 1;

            m_builtName = BuildTypeName();
        }

        private string BuildTypeName() {
            try {
                var typeNameFactory = new TypeNameFactory();
                var builder = typeNameFactory.CreateTypeNameBuilder();

                BuildTypeName(builder);

                if (AssemblyFullName != null)
                    builder.AddAssemblySpec(AssemblyFullName);

                return builder.ToString();

            } catch (COMException) {
                throw new InvalidOperationException();
            }
        }
        private void BuildTypeName(ITypeNameBuilder builder) {

            if (ElementType == null) {

                var name = Name;

                if (DeclaringType == null) {
                    if (Namespace != null)
                        name = Namespace + "." + name;

                } else {
                    DeclaringType.BuildTypeName(builder);
                }

                builder.AddName(name);

                if (HasGenericArguments) {
                    builder.OpenGenericArguments();

                    foreach (var genericArgument in GenericArguments()) {
                        builder.OpenGenericArgument();

                        genericArgument.BuildTypeName(builder);

                        var assemblyName = genericArgument.AssemblyFullName;
                        if (assemblyName != null)
                            builder.AddAssemblySpec(assemblyName);

                        builder.CloseGenericArgument();
                    }

                    builder.CloseGenericArguments();
                }

            } else {

                ElementType.BuildTypeName(builder);

                if (IsPointer)
                    builder.AddPointer();

                else if (IsSzArray)
                    builder.AddSzArray();

                else if (IsByRef)
                    builder.AddByRef();

                else
                    builder.AddArray(ArrayRank);
            }
        }

        public string Name {
            get { return m_name; }
        }
        public string Namespace {
            get { return m_namespace; }
        }
        public bool HasAssembly {
            get { return m_assemblyFullName != null; }
        }
        public string AssemblyFullName {
            get { return m_assemblyFullName; }
        }
        public string FullName {
            get {
                if (m_fullName == null) {

                    if (m_assemblyFullName != null)
                        m_fullName = MakeAssembly(null).FullName;

                    else
                        m_fullName = m_builtName;
                }
                return m_fullName;
            }
        }
        public string AssemblyQualifiedName {
            get {
                if (m_assemblyFullName == null)
                    return null;

                return m_builtName;
            }
        }

        public TypeName DeclaringType {
            get { return m_declaringType; }
        }

        public bool HasGenericArguments {
            get {
                if (m_genericArguments == null)
                    return false;

                return m_genericArguments.Any(); 
            }
        }
        public IEnumerable<TypeName> GenericArguments() {
            if (m_genericArguments == null)
                m_genericArguments = Enumerable.Empty<TypeName>();
            return m_genericArguments;
        }

        public bool HasElementType {
            get { return m_elementType != null; }
        }
        public TypeName ElementType {
            get { return m_elementType; }
        }
        public TypeName RootElementType {
            get {
                var result = this;
                while (result.HasElementType)
                    result = result.ElementType;
                return result;
            }
        }

        public bool IsByRef {
            get { return m_modifier == Modifier.ByRef; }
        }
        public bool IsPointer {
            get { return m_modifier == Modifier.Pointer; }
        }
        public bool IsArray {
            get { return m_modifier == Modifier.SzArray || m_modifier == Modifier.Array; }
        }
        public bool IsSzArray {
            get { return m_modifier == Modifier.SzArray; }
        }
        public int ArrayRank {
            get { return m_rank; }
        }

        public TypeName MakeByRef() {
            return new TypeName(this, Modifier.ByRef);
        }
        public TypeName MakePointer() {
            return new TypeName(this, Modifier.Pointer);
        }
        public TypeName MakeArray(int? rank = null) {
            if (rank == null)
                return new TypeName(this, Modifier.SzArray, 1);
            else
                return new TypeName(this, Modifier.Array, (int)rank);
        }
        public TypeName MakeAssembly(string assemblyFullName) {
            if (ElementType != null)
                return new TypeName(ElementType.MakeAssembly(assemblyFullName), m_modifier, m_rank);

            if (DeclaringType != null)
                return DeclaringType.MakeAssembly(assemblyFullName).MakeNestedType(Name, GenericArguments());

            return TypeName.Create(Name, Namespace, assemblyFullName, GenericArguments());
        }
        public TypeName MakeNestedType(string name, params TypeName[] genericArguments) {
            return MakeNestedType(name, (IEnumerable<TypeName>)genericArguments);
        }
        public TypeName MakeNestedType(string name, IEnumerable<TypeName> genericArguments = null) {
            return new TypeName(this, name, genericArguments);
        }
        public TypeName MakeNamespace(string ns) {
            if (ElementType != null) 
                return new TypeName(ElementType.MakeNamespace(ns), m_modifier, m_rank);

            if (DeclaringType != null) 
                return DeclaringType.MakeNamespace(ns).MakeNestedType(Name, GenericArguments());

            return TypeName.Create(Name, ns, AssemblyFullName);
        }
        public TypeName MakeGenericType(params TypeName[] genericArguments) {
            return MakeGenericType((IEnumerable<TypeName>)genericArguments);
        }
        public TypeName MakeGenericType(IEnumerable<TypeName> genericArguments) {
            if (HasElementType)
                throw new InvalidOperationException();

            if (DeclaringType == null)
                return TypeName.Create(Name, Namespace, AssemblyFullName, genericArguments);

            return new TypeName(DeclaringType, Name, genericArguments);
        }

        public override string ToString() {
            return AssemblyQualifiedName ?? FullName;
        }
        public override bool Equals(object obj) {
            if (obj == this)
                return true;

            var typeName = obj as TypeName;
            if (typeName == null)
                return false;

            return ToString() == typeName.ToString();
        }
        public override int GetHashCode() {
            return ToString().GetHashCode();
        }
    }
}
